<script lang="ts" module>
    import { getContext, onDestroy, onMount, setContext, untrack } from "svelte";
    import usezIndex from "../utils/usezIndex";
    import { getRandomIntInclusive, isColor } from "../utils/utils";
    import { useClassList } from "../utils/useClassList";
    import type { DropdownNode, DropdownProps } from "./Dropdown";
    import useAlignPostion from "../utils/useAlignPostion";
    import { useStyle } from "../utils/useStyle";
    import { useClickOutside } from "../utils/useClickOutside";
    import Portal from "svelte-portal";
    import usePortal from "../utils/usePortal";
    import DropdownMenu from "./DropdownMenu.svelte";
    import DropdownItem from "./DropdownItem.svelte";
    import { useMoveObserver } from "../utils/useMoveObserver";
    import { useTransition } from "../utils/useTransition";

    export const DropdownContextKey = Symbol("DropdownContextKey");
    export const useDropdownContext = () => getContext(DropdownContextKey);
</script>

<script lang="ts">
    let { visible = $bindable(false), ...props }: DropdownProps = $props();
    let _ = $state(0);
    let opened = $state(visible);

    let targetEle: any;
    let target: HTMLElement;
    let nextElementSibling: any;
    let wrap: any = $state();
    let lastEventTriggerTarget: any;
    let timer: any;
    let cleanup: any = null;

    const offset = $derived(props.offset || 8);
    const trigger = props.trigger || "hover";
    const align = props.align || "bottom";
    let placement = $state(align);
    const zindex = usezIndex();
    const revers = props.revers ?? true;
    const theme = isColor(props.theme) ? "" : props.theme;

    const transition = useTransition({
        el: () => wrap,
        startClass: "cm-dropdown-visible",
        activeClass: "cm-dropdown-open",
        onLeave: () => {
            opened = false;
            if (cleanup) {
                cleanup();
            }
        },
        onEnter: () => {
            opened = true;
            // 监控元素移动
            cleanup = useMoveObserver(wrap, () => {
                _ = getRandomIntInclusive(1, 1000000);
            });
        },
    });

    $effect(() => {
        const v = visible;
        if (v) {
            transition.enter();
        } else {
            transition.leave();
        }
    });

    const classList = $derived(
        useClassList(props, "cm-dropdown", {
            [`cm-dropdown-${theme}`]: theme,
            "cm-dropdown-with-arrow": props.arrow,
        })
    );

    let posStyle = $state("");

    // 防抖
    const clearDelayTimer = () => {
        if (timer) {
            clearTimeout(timer);
            timer = null;
        }
    };

    // 点击显示
    const onMouseClick = (e: any) => {
        if (!nextElementSibling.contains(e.target)) {
            return false;
        }
        if (props.handler) {
            const te = document.querySelector(props.handler);
            if (!te) {
                return;
            }
            if (!e.target.closest(props.handler) && !te.contains(e.target)) {
                return;
            }
        }

        if (props.disabled) {
            return;
        }

        e.preventDefault && e.preventDefault();
        e.stopPropagation && e.stopPropagation();
        targetEle = e.target;
        props.onmouseclick?.(e);

        const ret = props.onbeforedrop && props.onbeforedrop(visible);
        if (ret === undefined || ret) {
            // 触发的对象不一致，则重新定位，可能是使用handle进行触发,有多个触发元素的情况
            if (lastEventTriggerTarget !== e.target.closest(props.handler)) {
                _ = getRandomIntInclusive(1, 1000000);
            }
            visible = true;
            if (props.handler) {
                lastEventTriggerTarget = e.target.closest(props.handler);
            }
        }
    };

    const onMouseEnter = () => {
        if (props.disabled) {
            return;
        }
        if (trigger === "hover") {
            clearDelayTimer();
            visible = true;
            if (wrap) {
                // dropdown容器注册划走事件进行关闭
                wrap.removeEventListener("mouseleave", onMouseLeave);
                wrap.addEventListener("mouseleave", onMouseLeave, false);
            }
        }
    };

    const onMouseLeave = () => {
        if (props.disabled) {
            return;
        }
        if (trigger === "hover") {
            timer = setTimeout(() => {
                visible = false;
            }, 200);
        }
    };

    const getOffsetWidth = (align: string, rect: any, isLeft: boolean = false) => {
        if (align === "bottomRight" || align === "topRight") {
            return isLeft ? rect.width : 0;
        }
        if (align === "top" || align === "bottom") {
            return rect.width / 2;
        }
        if (align === "topLeft" || align === "bottomLeft") {
            return isLeft ? 0 : rect.width;
        }
        if (align === "left" || align === "leftTop" || align === "leftBottom") {
            return isLeft ? rect.width : 0;
        }
        if (align === "right" || align === "rightTop" || align === "rightBottom") {
            return isLeft ? 0 : rect.width;
        }
    };

    const getOffsetHeight = (align: string, rect: any) => {
        if (align === "leftBottom" || align === "rightBottom") {
            return 0;
        }
        if (align === "top" || align === "topLeft" || align === "topRight") {
            return 0;
        }
        if (align === "leftTop" || align === "rightTop") {
            return rect.height;
        }
        if (align === "left" || align === "right") {
            return rect.height / 2;
        }
        if (align === "bottom" || align === "bottomLeft" || align === "bottomRight") {
            return rect.height;
        }
    };

    const getPositionByPlacement = (align: DropdownProps["align"], te: any) => {
        const parent = te.offsetParent;
        if (!parent) {
            return;
        }
        const parentPos = parent.getBoundingClientRect();
        const pos: any = useAlignPostion(align as string, te);
        if (props.transfer) {
            const targetReact = te.getBoundingClientRect();
            pos.top = pos.top + document.documentElement.scrollTop;
            pos.left = pos.left + document.documentElement.scrollLeft;
            props.fixWidth ? (pos["min-width"] = targetReact.width + "px") : false;
        } else {
            pos.top = pos.top + parent.scrollTop - parentPos.top;
            pos.left = pos.left + parent.scrollLeft - parentPos.left;
        }
        return pos;
    };

    $effect(() => {
        _;
        if (opened && nextElementSibling) {
            untrack(() => {
                let te = nextElementSibling;
                if (props.handler) {
                    te = targetEle?.closest(props.handler);
                }
                if (!te) {
                    return;
                }
                const parent = te.offsetParent;
                if (!parent) {
                    return;
                }
                if (props.position) {
                    const pos = {
                        left: props.position.x + "px",
                        top: props.position.y + "px",
                    };
                    Object.assign(pos, props.style || {}, {
                        "--cui-dropdown-background-color": isColor(props.theme) ? props.theme : "",
                        "--cui-dropdown-text-color": props.color,
                        "--cui-dropdown-offset": `${offset}px`,
                    });
                    posStyle = useStyle(props, pos);
                    return;
                }
                const parentPos = parent.getBoundingClientRect();
                let pos: any = useAlignPostion(align, te);
                const originTop = pos.top;
                const originLeft = pos.left;
                if (props.transfer) {
                    const targetReact = te.getBoundingClientRect();
                    pos.top = pos.top + document.documentElement.scrollTop;
                    pos.left = pos.left + document.documentElement.scrollLeft;
                    props.fixWidth ? (pos["min-width"] = targetReact.width + "px") : false;
                } else {
                    pos.top = pos.top + parent.scrollTop - parentPos.top;
                    pos.left = pos.left + parent.scrollLeft - parentPos.left;
                }

                const rect = wrap.getBoundingClientRect();
                const offsetWidth = getOffsetWidth(align, rect);
                const leftOffsetWidth = getOffsetWidth(align, rect, true);
                const offsetHeight = getOffsetHeight(align, rect);
                const h = originTop + offsetHeight;
                const w = originLeft + offsetWidth;
                const lw = originLeft - offsetWidth;

                const containerHeight = window.innerHeight || document.documentElement.clientHeight;
                const containerWidth = window.innerWidth || document.documentElement.clientWidth;

                if (revers) {
                    let newAlign: DropdownProps["align"] = align;
                    if (h > containerHeight) {
                        if (align === "bottom" || align === "bottomLeft" || align === "bottomRight") {
                            newAlign = align.replace("bottom", "top") as DropdownProps["align"];
                        } else if (align === "leftTop" || align === "left") {
                            newAlign = "leftBottom" as DropdownProps["align"];
                        } else if (align === "right" || align === "rightTop") {
                            newAlign = "rightBottom" as DropdownProps["align"];
                        }
                    }
                    // align 为 top bottom topLeft bottomLeft right rightTop rightBottom 存在该情况
                    if (w > containerWidth - 5) {
                        if (align === "bottom" || align === "bottomLeft") {
                            newAlign = "bottomRight" as DropdownProps["align"];
                        } else if (align === "top" || align === "topLeft") {
                            newAlign = "topRight" as DropdownProps["align"];
                        } else if (align === "right") {
                            newAlign = "left" as DropdownProps["align"];
                        } else if (align === "rightTop") {
                            newAlign = "leftTop" as DropdownProps["align"];
                        } else if (align === "rightBottom") {
                            newAlign = "leftBottom" as DropdownProps["align"];
                        }
                    }
                    if (lw < 0) {
                        if (align === "bottom" || align === "bottomRight") {
                            newAlign = "bottomLeft" as DropdownProps["align"];
                        } else if (align === "top" || align === "topRight") {
                            newAlign = "topLeft" as DropdownProps["align"];
                        } else if (align === "left") {
                            newAlign = "right" as DropdownProps["align"];
                        } else if (align === "leftTop") {
                            newAlign = "rightTop" as DropdownProps["align"];
                        } else if (align === "leftBottom") {
                            newAlign = "rightBottom" as DropdownProps["align"];
                        }
                    }
                    if (newAlign !== align) {
                        pos = getPositionByPlacement(newAlign, te);
                        placement = newAlign!;
                    } else {
                        pos = getPositionByPlacement(align, te);
                        placement = align;
                    }
                }
                pos.top = pos.top + "px";
                pos.left = pos.left + "px";

                pos["z-index"] = zindex;

                posStyle = useStyle(
                    props,
                    Object.assign(pos, {
                        "--cui-dropdown-background-color": isColor(props.theme) ? props.theme : "",
                        "--cui-dropdown-text-color": props.color,
                        "--cui-dropdown-offset": `${offset}px`,
                    })
                );
            });
        }
    });

    export const update = () => (_ = getRandomIntInclusive(1, 1000000));

    let resizeTimer = 0;
    // 响应尺寸变化更新位置
    const onWrapEntry = (entry: ResizeObserverEntry) => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(() => {
            _ = getRandomIntInclusive(1, 1000000);
        }, 100);
    };

    let removeClickOutside: () => void;

    onMount(() => {
        nextElementSibling = target.nextElementSibling;
        target.parentNode?.removeChild(target);
        if (nextElementSibling) {
            if (trigger === "hover") {
                nextElementSibling.addEventListener("mouseenter", onMouseEnter, false);
                nextElementSibling.addEventListener("mouseleave", onMouseLeave, false);
            }
            if (trigger === "click" || trigger === "custom") {
                document.addEventListener("click", onMouseClick);
            }
            if (trigger === "contextMenu") {
                document.addEventListener("contextmenu", onMouseClick);
            }

            if (trigger === "click" || trigger === "contextMenu") {
                const other = props.handler ? nextElementSibling.querySelectorAll(props.handler) : nextElementSibling;
                removeClickOutside = useClickOutside([wrap, other], () => {
                    visible = false;
                });
            }

            const ro = new ResizeObserver((entries) => {
                entries.forEach((entry) => onWrapEntry(entry));
            });
            // 目标元素尺寸变化需要更新位置
            ro.observe(nextElementSibling);
            // 清除监听
            onDestroy(() => {
                ro.disconnect();
            });
        }
    });

    onDestroy(() => {
        if (nextElementSibling) {
            if (trigger === "hover") {
                nextElementSibling.removeEventListener("mouseenter", onMouseEnter);
                nextElementSibling.removeEventListener("mouseleave", onMouseLeave);
            }
            if (trigger === "click" || trigger === "custom") {
                document.removeEventListener("click", onMouseClick);
            }
            if (trigger === "contextMenu") {
                document.removeEventListener("contextmenu", onMouseClick);
            }
        }
        removeClickOutside && removeClickOutside();
        if (cleanup) {
            cleanup();
        }
    });

    const onSelect = (name: string, data?: any) => {
        props.onselect?.(name, data);
        wrap.removeEventListener("mouseleave", onMouseLeave);
        visible = false;
    };

    const id = "cm-dropdown-portal";

    setContext(DropdownContextKey, {
        onSelect,
        gradient: props.gradient,
        color: props.color,
    });
</script>

{#snippet renderMenu(arr?: DropdownNode[])}
    {#if arr}
        <DropdownMenu>
            {#each arr as item (item.name)}
                {@const { title, children, ...rest } = item}
                <DropdownItem {...rest} data={item} arrow={!!children?.length}>
                    {#if typeof title === "function"}
                        {@render title(item)}
                    {:else}
                        {title}
                    {/if}
                    {#if children?.length}
                        {@render renderMenu(children)}
                    {/if}
                </DropdownItem>
            {/each}
        </DropdownMenu>
    {/if}
{/snippet}

{#snippet inner()}
    <!-- svelte-ignore a11y_no_static_element_interactions -->
    <div style={posStyle} class={classList} x-placement={placement} onmouseenter={onMouseEnter} bind:this={wrap}>
        {#if props.menu}
            {@render props.menu?.()}
        {/if}
        {@render renderMenu(props.data)}
        {#if props.arrow}
            <svg width="24" height="8" xmlns="http://www.w3.org/2000/svg" class="cm-dropdown-arrow">
                <path d="M0.5 0L1.5 0C1.5 4, 3 5.5, 5 7.5S8,10 8,12S7 14.5, 5 16.5S1.5,20 1.5,24L0.5 24L0.5 0z" opacity="1" />
                <path d="M0 0L1 0C1 4, 2 5.5, 4 7.5S7,10 7,12S6 14.5, 4 16.5S1,20 1,24L0 24L0 0z" />
            </svg>
        {/if}
    </div>
{/snippet}

<span bind:this={target} style="display: none"></span>
{@render props.children?.()}
{#if props.transfer}
    <Portal target={usePortal(id, id)}>
        {@render inner()}
    </Portal>
{:else}
    {@render inner()}
{/if}
